Cheatsheet [Front-End]

The 20% of the JS journal + prep plan an interviewer actually probes, with runnable code for each. Study this; skim the big files only where something here feels shaky. Working reference build: react/FleetList.jsx.

1 ยท State rules โ€” most-tested React knowledge

  • Minimal state: store only user-changed data. Winner, sunk status, filtered lists = derived in render, never stored. Storing derived state (then syncing it with useEffect) is the classic interview red flag.
  • State is stale until next render. Compute with locals first, call setters at the end of the handler.
  • Functional update when new depends on old: setX(prev => ...). Multiple plain setX(x+1) calls in one handler collapse to one increment โ€” batching.
  • Never mutate. New arrays/objects every update; React compares by reference and skips re-renders on same reference.
  • useReducer when many action types hit one structure (game moves, undo): one pure (state, action) => newState.
  • Component body order: state โ†’ derived โ†’ handlers โ†’ JSX.

2 ยท Rendering a grid โ€” the full pattern

Three parts: build the 2D state, render it with nested maps inside CSS grid, update one cell immutably. The CSS trick: the wrapper is one grid container with repeat(COLS, ...), and you render a flat stream of cells into it โ€” row divs are unnecessary. Keys combine row and column since grid cells have no natural id.
// 1. Build the 2D state โ€” Array.from gives each row its OWN array.
//    Array(3).fill([]) is a bug: every row would be the same array object.
const ROWS = 3, COLS = 4;
const [cells, setCells] = useState(() =>
  Array.from({ length: ROWS }, () => Array(COLS).fill('empty'))
);

// 2. Render โ€” outer map = rows, inner map = cells in that row.
//    CSS grid wraps the flat children into COLS columns automatically.
return (
  <div style={{
    display: 'grid',
    gridTemplateColumns: `repeat(${COLS}, 48px)`,
    gap: 4,
  }}>
    {cells.map((row, r) =>
      row.map((cell, c) => (
        <button
          key={`${r}-${c}`}          // stable for a fixed-size grid
          onClick={() => handleClick(r, c)}
          style={{ height: 48, background: colorFor(cell) }}
        >
          {cell === 'hit' ? 'โœ•' : ''}
        </button>
      ))
    )}
  </div>
);

// 3. Update ONE cell immutably โ€” copy only the row you touch.
//    Untouched rows keep their reference (matters later for React.memo).
function handleClick(r, c) {
  setCells(prev => prev.map((row, ri) =>
    ri !== r
      ? row                                    // untouched row: same reference
      : row.map((cell, ci) => ci !== c ? cell : nextState(cell))
  ));
}

// State machine as data โ€” no if/else chain
const CYCLE = { empty: 'hit', hit: 'miss', miss: 'empty' };
const nextState = cell => CYCLE[cell];
  • Why useState(() => ...): the function form (lazy init) builds the array once, not on every render.
  • Why not row[r][c] = x: mutation keeps the same outer reference, React may skip the re-render โ€” and even when it renders, memoized children won't update.
  • Say out loud: "untouched rows keep their reference, so if I memoize rows or cells later, only the changed one re-renders."

3 ยท Controlled input โ€” the two-way wiring

Controlled = React state is the single source of truth. The input shows exactly value; typing fires onChange, which updates state, which re-renders the input with the new value. Forget onChange and the input is frozen; forget value and it's uncontrolled (DOM owns it, read via ref).
const [query, setQuery] = useState('');

<input
  value={query}                              // state โ†’ UI
  onChange={e => setQuery(e.target.value)}   // UI โ†’ state
  placeholder="Search"
/>

// Same idea for select
<select value={status} onChange={e => setStatus(e.target.value)}>
  <option value="all">All</option>
  <option value="stopped">Stopped</option>
</select>

// Form submit: preventDefault or the page reloads
<form onSubmit={e => { e.preventDefault(); addItem(query); }}>

4 ยท useCallback + React.memo โ€” the pair

Yes โ€” the handler is a function, and a component recreates every function defined in its body on every render. New function = new reference = a memoized child sees a "changed" prop and re-renders anyway. useCallback returns the same function reference across renders, which is what lets React.memo actually skip work. One is useless without the other.
// Child: React.memo skips re-render if all props are === to last time
const Cell = React.memo(function Cell({ state, onClick }) {
  return <button onClick={onClick}>{state}</button>;
});

// Parent โ€” WITHOUT useCallback: new function each render,
// so every Cell re-renders and React.memo does nothing:
const handleClick = (r, c) => { /* ... */ };   // โœ— new ref each render

// WITH useCallback: same reference until deps change
const handleClick = useCallback((r, c) => {
  setCells(prev => updateCell(prev, r, c));  // functional update โ‡’
}, []);                                        // no deps needed at all

// Trap: inline arrow in JSX recreates the ref and defeats memo again.
<Cell onClick={() => handleClick(r, c)} />   // โœ— new ref per render
// Fix: pass r,c as props and let Cell call onClick(r, c) itself.
  • Interview line: "I'd only add this after noting all 100 cells re-render per click โ€” at this size it's optional, and I'd measure first."
  • useMemo is the same idea for values; useCallback(fn, deps) โ‰ก useMemo(() => fn, deps).

5 ยท Debounce for a search input โ€” two ways

Debounce = fire once after calls stop for N ms (search input). Throttle = fire at most once per N ms while calls keep coming (scroll, resize, mousemove). Say aloud: "debounce waits for silence; throttle samples during noise." The React trap: the debounced fn must be created once, not on every render, or each render gets a fresh timer and the debounce never holds.

A ยท Debounce factory (lodash or hand-rolled) + useMemo

// CoderPad has npm โ€” lodash just works. No lodash? The hand-rolled version is 5 lines.
import { debounce } from 'lodash';

function debounce(fn, ms) {   // hand-rolled โ€” know it cold
  let timer;                 // timer id lives in the closure (the persistent slot)
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}

// In React โ€” useMemo [] so the SAME debounced fn survives re-renders:
const debouncedSetQuery = useMemo(
  () => debounce((value) => setQuery(value), 300),
[]);

const onSearch = (e) => {
  setInputValue(e.target.value);      // controlled input: instant
  debouncedSetQuery(e.target.value);  // filtering: after the pause
};

// cancel a pending call if the component unmounts (lodash gives .cancel())
useEffect(() => () => debouncedSetQuery.cancel(), [debouncedSetQuery]);

// โ”€โ”€ Why useMemo and NOT useCallback here (common instinct โ€” it's a function!) โ”€โ”€
// useMemo CALLS its fn and stores the RETURN value; useCallback stores the fn itself.
// You want debounce()'s OUTPUT (built once), which is a value โ†’ that's useMemo's job.
useMemo(() => debounce(setQuery, 300), []);   // โœ“ debounce() runs ONCE (inside the arrow)
useCallback(debounce(setQuery, 300), []);   // โœ— debounce() runs EVERY render, extras thrown away; eslint warns
// useCallback is for a fn you define INLINE: useCallback((v) => setQuery(v), []) โ€” but that's NOT debounced.

B ยท Timer ref + useEffect cleanup (simplest to write live)

// No helper, no useMemo. ref.current holds anything (timer id, DOM node...);
// persists across renders, and writing to it does NOT re-render.
// That's why the id survives between keystrokes.
const timerRef = useRef(null);

const onSearch = (e) => {
  const value = e.target.value;
  setInputValue(value);              // input: instant
  clearTimeout(timerRef.current);    // cancel PREVIOUS keystroke's timer
  timerRef.current = setTimeout(() => setQuery(value), 300);  // arm new, save id
};

// clear a pending timer if the component unmounts mid-countdown
useEffect(() => () => clearTimeout(timerRef.current), []);
  • Which to use: B (timer ref) for one search box โ€” self-contained, nothing to import, easy to narrate. A (factory) when debounce is reused or the prompt is literally "implement debounce."
  • Timer-ref traps (all make debounce a no-op): storing the id in a local const timer instead of the ref โ†’ nothing survives to cancel, every keystroke fires. Calling clearTimeout(timer) in the SAME handler on the timer you just set โ†’ cancels it instantly, nothing ever fires. The clear must reach a timer from an earlier call, so the id needs a persistent slot (ref).

6 ยท BFS / DFS in JavaScript

Waymo scope: level-order on a tree, flood-fill on a grid. BFS = queue (shift), DFS = recursion or stack (pop). Grid version needs bounds check + visited/mark.
// BFS โ€” level order on a tree (returns [[level0], [level1], ...])
function levelOrder(root) {
  if (!root) return [];
  const result = [], queue = [root];
  while (queue.length) {
    const size = queue.length;      // snapshot = this level
    const level = [];
    for (let i = 0; i < size; i++) {
      const node = queue.shift();
      level.push(node.val);
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    result.push(level);
  }
  return result;
}

// DFS โ€” flood fill on a grid (recursion; mark visited by mutating)
const DIRS = [[0,1],[0,-1],[1,0],[-1,0]];
function flood(grid, r, c) {
  if (r < 0 || r >= grid.length ||
      c < 0 || c >= grid[0].length ||
      grid[r][c] !== '1') return;
  grid[r][c] = '0';                   // mark visited
  for (const [dr, dc] of DIRS) flood(grid, r + dr, c + dc);
}
  • queue.shift() is O(n) โ€” fine in interviews; mention "I'd use a head-index or deque for large n."
  • Mutating the algorithm's own grid copy is fine โ€” just never mutate React state this way.

7 ยท reduce โ€” especially reduce-as-map

Signature: arr.reduce((acc, item) => newAcc, initial). The accumulator is whatever you want โ€” number, object, Map. "Reduce into an object" is how you group and count in JS (Python's defaultdict equivalent).
// Sum โ€” the trivial case
const total = nums.reduce((acc, n) => acc + n, 0);

// Count by key โ†’ { stopped: 3, active: 2 }  ("reduce as map")
const counts = vehicles.reduce((acc, v) => {
  acc[v.status] = (acc[v.status] ?? 0) + 1;
  return acc;                       // โ† forgetting this = classic bug
}, {});

// Group by key โ†’ { 'sf-1': [v1, v4], 'ph-1': [v3] }
const byZone = vehicles.reduce((acc, v) => {
  (acc[v.zoneId] ??= []).push(v);   // ??= creates the bucket once
  return acc;
}, {});

// Index by id โ†’ O(1) lookup table (Map keeps non-string keys)
const byId = new Map(vehicles.map(v => [v.id, v]));
  • Rule of thumb to say aloud: array โ†’ array is map/filter; array โ†’ one thing (object, Map, number) is reduce.
  • If the reduce body needs more than ~3 lines, a plain for...of loop is more readable โ€” saying that is a point in your favor.

โš‘ Gaps โ€” in neither journal nor prep plan, likely to come up

  • useEffect dependency arrays. Journal only covers cleanup. Know: [] = mount once, [x] = when x changes, none = every render. For a TPS build you rarely need useEffect at all โ€” say "no effect needed, everything derives from state" and score points.
  • Immutable nested/2D updates. Biggest practical gap given grids โ€” now covered in panel 2. Drill until automatic.
  • Lifting state up. Two siblings need the same data โ†’ state moves to the parent. One sentence, be able to say it.
  • Index-as-key pitfall. Journal says "needs a key" but not why index breaks: on reorder/delete React reuses the wrong DOM/state. Fine only for fixed-size never-reordered grids.
  • Typing React in TS. Journal has enums only. Know: interface Props { state: Cell; onClick: () => void }, useState<Cell[][]>(...), e: React.ChangeEvent<HTMLInputElement>. (CoderPad squiggles are cosmetic; // @ts-nocheck if noisy.)
  • Controlled vs uncontrolled as a concept โ€” panel 3 covers controlled; uncontrolled = DOM owns the value, read via ref on submit.
  • Custom hooks / useContext: mention-only. "I'd extract the debounce into a useDebouncedValue hook" is a strong closer; don't study deeply.

Focus โ€” what Waymo actually asks

  • Grids: yes, drill them. Battleship board (reported 2026), "Random Grid Token Placement" (TPS), Set Matrix Zeroes and Sparse Matrix in the pool. Grid state + click cycling + derived status is the highest-value rehearsal โ€” panel 2 is that whole build in miniature.
  • Equal weight: filterable data list. Fleet-monitoring role โ†’ vehicle list with status dropdown, sort, debounced search. Full worked version: react/FleetList.jsx + react/fleet-list-question.html. Build both once, timed, before the call.
  • Skip this week: DP, backtracking, heaps, hard graphs, polyfill drills. Light BFS/DFS only (panel 6 is the ceiling).
  • Rehearse the mouth, not just the hands: state shape out loud, Big-O out loud, the memoization talk track (panel 4) out loud.
Pair with ui_interview_playbook.html for the in-interview sequence. Two timed builds > ten more read-throughs.